﻿// Copyright (c) 2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Util.Forms
{
    /// <summary> 
    /// Provides a task scheduler that allows the user to precisely control
    /// the way tasks are scheduled.
    /// </summary> 
    public sealed class ManualTaskScheduler : TaskScheduler, IDisposable
    {
#if false
        /// <summary>
        /// Indicates whether the current thread is a worker thread created
        /// by this scheduler. This flag is used by TryExecuteTaskInline()
        /// to allow inlining if called from within a worker thread.
        /// </summary>
        [ThreadStatic]
        private static bool thisThreadIsWorkerThread = false;
#endif

        /// <summary>
        /// An internal object used to synchronize access.
        /// </summary>
        private object syncObject = new Int32();

        /// <summary>
        /// The internal task queue. Tasks are always dequeued from the head
        /// of the list, but they may be added either to the head or to the
        /// tail of the list to achieve LIFO or FIFO. To change this member,
        /// a lock must be acquired on syncObject.
        /// </summary>
        private LinkedList<Task> taskQueue = new LinkedList<Task>();

        /// <summary>
        /// Signal that is set when the task queue is not empty, and reset
        /// when the task queue is empty. To change this member, a lock must
        /// be acquired on syncObject. This signal is disposed when the last
        /// worker thread exits.
        /// </summary>
        private ManualResetEvent taskAvailable = new ManualResetEvent(false);

        /// <summary>
        /// Flag that indicates whether this scheduler should be disposed.
        /// After the Dispose() method is called, this member is set to true
        /// and the worker threads exit when there are no more items in the
        /// task queue.
        /// </summary>
        private bool disposed = false;

        /// <summary>
        /// The number of worker threads to create. Default is 1.
        /// </summary>
        private int numThreads = 1;

        /// <summary>
        /// Creates a ManualTaskScheduler with the specified queuing policy.
        /// </summary>
        /// <param name="policy"></param>
        public ManualTaskScheduler(/*ManualTaskSchedulerPolicy policy*/)
        {
            // Create a dedicated background thread to execute the tasks.
            Thread worker = new Thread(WorkerRoutine);
            worker.Name = "ManualTaskScheduler[" + this.Id + "].Worker";
            worker.IsBackground = true;
            worker.Start();
        }

        /// <summary>
        /// Disposes the scheduler. The tasks already queued are executed, 
        /// but no more new tasks are allowed to be queued.
        /// </summary>
        public void Dispose()
        {
            lock (syncObject)
            {
                disposed = true;
                taskAvailable.Set(); // unblock waiting worker threads
            }
        }

        /// <summary>
        /// A worker routine that waits for and executes tasks.
        /// </summary>
        private void WorkerRoutine()
        {
#if false
            // Set the flag to enable inlining of tasks onto this thread.
            thisThreadIsWorkerThread = true;
#endif

            // Process tasks in the task queue until the thread terminates.
            while (true)
            {
                // Try get a task from the queue.
                Task task = null;
                lock (syncObject)
                {
                    if (taskQueue.Count == 0) // no more tasks
                    {
                        if (disposed) // scheduler should exit
                        {
                            if (--numThreads == 0)
                            {
                                taskAvailable.Dispose();
                            }
                            return;
                        }
                        taskAvailable.Reset(); // mark task queue as empty
                    }
                    else
                    {
                        task = taskQueue.First.Value;
                        taskQueue.RemoveFirst();
                    }
                }

                // If no task is available, wait for a signal to try again.
                if (task == null)
                {
                    taskAvailable.WaitOne();
                    continue;
                }

                // Execute the task we pulled out of the queue. Silently eats
                // any exceptions because there's not much we can do.
                try
                {
                    base.TryExecuteTask(task);
                }
                catch (Exception) { }

                // This is for testing only.
                // Thread.Sleep(500);
            }
        }

        /// <summary>
        /// Gets the maximum concurrency level this scheduler supports.
        /// </summary> 
        public override int MaximumConcurrencyLevel
        {
            get { return numThreads; }
        }

        /// <summary>
        /// Queues a task to the scheduler.
        /// </summary> 
        /// <param name="task">The task to be queued.</param>
        /// <exception cref="ArgumentNullException">The task argument is null.
        /// </exception>
        protected override void QueueTask(Task task)
        {
            if (task == null)
                throw new ArgumentNullException("task");

#if DEBUG
            //System.Diagnostics.Debug.WriteLine(string.Format(
            //    "ManualTaskScheduler.QueueTask({0})", task.Id));
#endif
            // If the task is created with PreferFairness option, schedule
            // it to the end of the queue (FIFO). Otherwise, schedule it
            // to the head of the queue (LIFO).
            bool fifo = task.CreationOptions.HasFlag(
                TaskCreationOptions.PreferFairness);

            // Add the task to the queue.
            lock (syncObject)
            {
                if (disposed)
                    throw new ObjectDisposedException("ManualTaskScheduler");

                if (fifo)
                    taskQueue.AddLast(task);
                else
                    taskQueue.AddFirst(task);

                // Signals that a task is available.
                taskAvailable.Set();
            }
        }

        /// <summary>
        /// Try execute the given task synchronously in this call.
        /// </summary>
        /// <param name="task">The task to be executed.</param>
        /// <param name="taskWasPreviouslyQueued"></param>
        /// <returns>Whether the task could be executed on the current thread.</returns> 
        protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
        {
#if false
            // We only synchronouly execute a task if called from one of our
            // worker threads.
            if (thisThreadIsWorkerThread)
            {
            // If the task was previously queued, remove it from the queue 
            //if (taskWasPreviouslyQueued) TryDequeue(task);
                return base.TryExecuteTask(task);
            }
#endif
            return false;
        }

        /// <summary>
        /// Removes a previously scheduled task from the scheduler.
        /// </summary> 
        /// <param name="task">The task to be removed.</param>
        /// <returns>true if the task is found and removed.</returns> 
        protected override bool TryDequeue(Task task)
        {
#if DEBUG
            //System.Diagnostics.Debug.WriteLine(string.Format(
            //    "ManualTaskScheduler.TryDequeue({0})", task.Id));
#endif
            lock (syncObject)
            {
                return taskQueue.Remove(task);
            }
        }

        /// <summary>
        /// Requests a scheduled task to be executed as soon as possible.
        /// </summary>
        public bool Prioritize(Task task)
        {
#if DEBUG
            //System.Diagnostics.Debug.WriteLine(string.Format(
            //    "ManualTaskScheduler.Prioritize({0})", task.Id));
#endif
            lock (syncObject)
            {
                if (taskQueue.Remove(task))
                {
                    taskQueue.AddFirst(task);
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// For debugger support only, gets an enumerable of the tasks
        /// currently queued in the scheduler waiting to be executed.
        /// </summary>
        protected override IEnumerable<Task> GetScheduledTasks()
        {
            bool lockTaken = false;
            try
            {
                Monitor.TryEnter(syncObject, ref lockTaken);
                if (lockTaken)
                    return taskQueue.ToArray();
                else
                    throw new NotSupportedException();
            }
            finally
            {
                if (lockTaken)
                    Monitor.Exit(syncObject);
            }
        }
    }

    public enum ManualTaskSchedulerPolicy
    {
        FirstInFirstOut = 0,
        LastInFirstOut = 1
    }
}
